﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Media;

namespace Red.Controls
{
    /// <summary>
    /// A basic ruler implementation. 
    /// 
    /// It is simple ruler that can draw itself only. This means it doesn't support any user 
    /// interactions like scroling and zooming. Additional behaviors should be done viva an external 
    /// controler.
    /// </summary>
    public class Ruler : FrameworkElement
    {
        #region Readonly static fields
        // Dispaly size of division marks
        // 0        10
        // |....i....|
        private readonly static int _majorMarkSizePx = 8;   //  | - a major mark
        private readonly static int _middleMarkSizePx = 6;  //  i   
        private readonly static int _minorMarkSizePx = 3;   //  .

        private readonly static CultureInfo _fontCultureInfo = CultureInfo.GetCultureInfo( "en-us" );
        private Typeface _fontFace = new Typeface( "Verdana" );
        #endregion

        #region Data
        private Pen _rulerMajorIntervalPen = new Pen( new SolidColorBrush( Colors.Black ), 1.25 );
        private Pen _rulerMiddleIntervalPen = new Pen( new SolidColorBrush( Colors.Black ), 0.50 );
        private Pen _rulerMinorIntervalPen = new Pen( new SolidColorBrush( Colors.Black ), 0.25 );
        private Pen _rulerPen = new Pen( new SolidColorBrush( Colors.Black ), 1 );

        private Point _basePoint;
        private double _pixelToValueRatio = 1;

        /// <summary>
        /// Middle mark is displayed only when there is even number of major interval divisions
        /// </summary>
        private bool _dispalyMiddleMark = false;
        #endregion

        #region Ctr
        public Ruler()
        {
            ClipToBounds = true;
            SnapsToDevicePixels = true;

            SizeChanged += Ruler_SizeChanged;
            UpdateCurrentEndValue();
        }

        #endregion

        #region Properties
        public FontFamily FontFamily
        {
            get { return (FontFamily)GetValue( FontFamilyProperty ); }
            set { SetValue( FontFamilyProperty, value ); }
        }

        public double FontSize
        {
            get { return (double)GetValue( FontSizeProperty ); }
            set { SetValue( FontSizeProperty, value ); }
        }

        public Brush Background
        {
            get { return (Brush)GetValue( BackgroundProperty ); }
            set { SetValue( BackgroundProperty, value ); }
        }

        public Brush Foreground
        {
            get { return (Brush)GetValue( ForegroundProperty ); }
            set { SetValue( ForegroundProperty, value ); }
        }

        public double MinValue
        {
            get { return (double)GetValue( MinValueProperty ); }
            set { SetValue( MinValueProperty, value ); }
        }

        public double MaxValue
        {
            get { return (double)GetValue( MaxValueProperty ); }
            set { SetValue( MaxValueProperty, value ); }
        }

        /// <summary>
        /// The first value of ruler that is "visible" on the screen. The value can be not aligned with ruler marks. 
        /// </summary>
        public double StartValue
        {
            get { return (double)GetValue( StartValueProperty ); }
            set { SetValue( StartValueProperty, value ); }
        }

        /// <summary>
        /// Major interval unit for example 10cm or 100ms 
        /// </summary>
        public double MajorIntervalUnit
        {
            get { return (double)GetValue( MajorIntervalUnitProperty ); }
            set { SetValue( MajorIntervalUnitProperty, value ); }
        }

        /// <summary>
        /// A space in pixels that will take a major interval unit on the screen
        /// </summary>
        public double MajorIntervalSizePx
        {
            get { return (double)GetValue( MajorIntervalSizePxProperty ); }
            set { SetValue( MajorIntervalSizePxProperty, value ); }
        }

        /// <summary>
        /// How many times a major interval will be divided (1 means no division)
        /// </summary>
        public int MajorIntervalDivision
        {
            get { return (int)GetValue( MajorIntervalDivisionProperty ); }
            set { SetValue( MajorIntervalDivisionProperty, value ); }
        }

        public double MinorIntervalUnit
        {
            get { return (double)GetValue( MinorIntervalUnitProperty ); }
            protected set { SetValue( MinorIntervalUnitPropertyKey, value ); }
        }

        public double MinorIntervalSizePx
        {
            get { return (double)GetValue( MinorIntervalSizePxProperty ); }
            protected set { SetValue( MinorIntervalSizePxPropertyKey, value ); }
        }

        public double PixelToValueRatio
        {
            get { return _pixelToValueRatio;  }
        }

        public enum LayoutTypes
        {
            Bottom,
            Top,
            Left,
            Right
        }

        /// <summary>
        /// Internal Layout of the ruler. 
        ///
        /// It points where the ruler line will be placed. For example Left means that the ruler line will be placed near the left edge of the control area.
        /// </summary>
        public LayoutTypes Layout
        {
            get { return (LayoutTypes)GetValue( LayoutProperty ); }
            set { SetValue( LayoutProperty, value ); }
        }

        public bool DrawRulerLine
        {
            get { return (bool)GetValue( DrawRulerLineProperty ); }
            set { SetValue( DrawRulerLineProperty, value ); }
        }

        public bool DrawValues
        {
            get { return (bool)GetValue( DrawValuesProperty ); }
            set { SetValue( DrawValuesProperty, value ); }
        }

        public bool DrawStretchedIntervalMarks
        {
            get { return (bool)GetValue( DrawStretchedIntervalMarksProperty ); }
            set { SetValue( DrawStretchedIntervalMarksProperty, value ); }
        }

        public bool DrawValidRange
        {
            get { return (bool)GetValue( DrawValidRangeProperty ); }
            set { SetValue( DrawValidRangeProperty, value ); }
        }

        public Brush ValidRangeBackground
        {
            get { return (Brush)GetValue( ValidRangeBackgroundProperty ); }
            set { SetValue( ValidRangeBackgroundProperty, value ); }
        }

        public double CurrentEndValue { get; private set; }
        #endregion

        #region Dependency properites objects
        public static readonly DependencyProperty FontSizeProperty =
            TextElement.FontSizeProperty.AddOwner( typeof( Ruler ),
                new FrameworkPropertyMetadata( SystemFonts.MessageFontSize, FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender ) );

        public static readonly DependencyProperty FontFamilyProperty =
               TextElement.FontFamilyProperty.AddOwner( typeof( Ruler ),
                       new FrameworkPropertyMetadata( new FontFamily( "Verdana" ), FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender, OnFaceFamilyChange ) );

        private static void OnFaceFamilyChange( DependencyObject d, DependencyPropertyChangedEventArgs e )
        {
            Ruler ruler = (Ruler)d;
            FontFamily family = (FontFamily)e.NewValue;

            if ( family != null )
            {
                ICollection<Typeface> typefaces = family.GetTypefaces();
                ruler._fontFace = typefaces.ToArray()[0];
            }
        }

        public static readonly DependencyProperty BackgroundProperty =
            Panel.BackgroundProperty.AddOwner( typeof( Ruler ),
                new FrameworkPropertyMetadata( new SolidColorBrush( Colors.Transparent ), FrameworkPropertyMetadataOptions.AffectsRender ) );

        public static readonly DependencyProperty ForegroundProperty =
            TextElement.ForegroundProperty.AddOwner( typeof( Ruler ),
                new FrameworkPropertyMetadata( SystemColors.ControlTextBrush, FrameworkPropertyMetadataOptions.AffectsRender, OnForegroundPropertyChange ) );

        private static void OnForegroundPropertyChange( DependencyObject d, DependencyPropertyChangedEventArgs e )
        {
            Ruler ruler = (Ruler)d;

            ruler._rulerPen = new Pen( (Brush)e.NewValue, 1 );
            ruler._rulerMajorIntervalPen = new Pen( (Brush)e.NewValue, 1 );
            ruler._rulerMiddleIntervalPen = new Pen( (Brush)e.NewValue, 0.25 );
            ruler._rulerMinorIntervalPen = new Pen( (Brush)e.NewValue, 0.1 );
        }

        private static readonly DependencyProperty MinValueProperty =
            DependencyProperty.Register( "MinValue", typeof( double ), typeof( Ruler ),
                new FrameworkPropertyMetadata( 0.0, FrameworkPropertyMetadataOptions.AffectsRender ) );

        private static readonly DependencyProperty MaxValueProperty =
            DependencyProperty.Register( "MaxValue", typeof( double ), typeof( Ruler ),
                new PropertyMetadata( 100.0 ) );

        private static readonly DependencyProperty StartValueProperty =
            DependencyProperty.Register( "StartValue", typeof( double ), typeof( Ruler ),
                new FrameworkPropertyMetadata( 0.0, FrameworkPropertyMetadataOptions.AffectsRender, UpdateEndValue ) );

        private static readonly DependencyPropertyKey MinorIntervalUnitPropertyKey =
            DependencyProperty.RegisterReadOnly( "MinorIntervalUnit", typeof( double ), typeof( Ruler ),
                new FrameworkPropertyMetadata( 1.0, FrameworkPropertyMetadataOptions.AffectsRender ) );

        public static readonly DependencyProperty MinorIntervalUnitProperty = MinorIntervalUnitPropertyKey.DependencyProperty;

        private static readonly DependencyPropertyKey MinorIntervalSizePxPropertyKey =
            DependencyProperty.RegisterReadOnly( "MinorIntervalSizePx", typeof( double ), typeof( Ruler ),
                new FrameworkPropertyMetadata( 10.0, FrameworkPropertyMetadataOptions.AffectsRender ) );

        public static readonly DependencyProperty MinorIntervalSizePxProperty = MinorIntervalSizePxPropertyKey.DependencyProperty;

        public static readonly DependencyProperty MajorIntervalUnitProperty =
            DependencyProperty.Register( "MajorIntervalUnit", typeof( double ), typeof( Ruler ),
                new FrameworkPropertyMetadata( 10.0, FrameworkPropertyMetadataOptions.AffectsRender, UpdateEndValue ) );

        public static readonly DependencyProperty MajorIntervalSizePxProperty =
            DependencyProperty.Register( "MajorIntervalSizePx", typeof( double ), typeof( Ruler ),
                new FrameworkPropertyMetadata( 100.0, FrameworkPropertyMetadataOptions.AffectsRender, UpdateEndValue ) );

        public static readonly DependencyProperty MajorIntervalDivisionProperty =
            DependencyProperty.Register( "MajorIntervalDivision", typeof( int ), typeof( Ruler ),
                new FrameworkPropertyMetadata( 10, FrameworkPropertyMetadataOptions.AffectsRender ) );

        private static readonly DependencyProperty LayoutProperty =
            DependencyProperty.Register( "Layout", typeof( LayoutTypes ), typeof( Ruler ),
                new FrameworkPropertyMetadata( LayoutTypes.Top, FrameworkPropertyMetadataOptions.AffectsRender ) );
        
        public static readonly DependencyProperty DrawStretchedIntervalMarksProperty =
            DependencyProperty.Register( "DrawStretchedIntervalMarks", typeof( bool ), typeof( Ruler ),
                new FrameworkPropertyMetadata( false, FrameworkPropertyMetadataOptions.AffectsRender ) );

        public static readonly DependencyProperty DrawRulerLineProperty =
            DependencyProperty.Register( "DrawRulerLine", typeof( bool ), typeof( Ruler ),
                new FrameworkPropertyMetadata( true, FrameworkPropertyMetadataOptions.AffectsRender ) );

        public static readonly DependencyProperty DrawValuesProperty =
            DependencyProperty.Register( "DrawValues", typeof( bool ), typeof( Ruler ),
                new FrameworkPropertyMetadata( true, FrameworkPropertyMetadataOptions.AffectsRender ) );

        public static readonly DependencyProperty DrawValidRangeProperty =
            DependencyProperty.Register( "DrawValidRange", typeof( bool ), typeof( Ruler ),
                new FrameworkPropertyMetadata( false, FrameworkPropertyMetadataOptions.AffectsRender ) );

        public static readonly DependencyProperty ValidRangeBackgroundProperty =
             DependencyProperty.Register( "ValidRangeBackground", typeof( Brush ), typeof( Ruler ),
                new FrameworkPropertyMetadata( new SolidColorBrush( Colors.DarkGray ), FrameworkPropertyMetadataOptions.AffectsRender ) );
        #endregion

        #region Public methods

        public double ValueToScreenOffset( double value )
        {
            return Math.Abs( value - StartValue ) * _pixelToValueRatio;
        }

        public bool IsValueVisibleOnScreen( double value )
        {
            if ( value < StartValue || CurrentEndValue < value )
            {
                return false;
            }
            return true;
        }

        #endregion Public methods

        #region FrameworkElement overrides
        protected override Size MeasureOverride( Size availableSize )
        {
            Size minSize = MeasureValueLabelSize( MinValue );
            Size maxSize = MeasureValueLabelSize( MaxValue );

            double width = Math.Max( minSize.Width, maxSize.Width );
            double height = Math.Max( minSize.Height, maxSize.Height );

            if ( HasHorizontalLayout() )
            {
                if ( Double.IsInfinity( availableSize.Width ) )
                {
                    double range = MaxValue - MinValue;
                    double rulerWidth = ( range / MajorIntervalUnit ) * MajorIntervalSizePx;

                    return new Size( rulerWidth, height );
                }
            }
            return new Size( width, height );
        }
        #endregion

        #region UIElement overrides
        protected override void OnRender( DrawingContext dc )
        {
            base.OnRender( dc );
         
            SetRulerState();
            RecomputeRulerStartingPosion();

            double currentValue; // value of the first visible mark on the screen
            double currentScreenOffset; // a screen offset to a postion where the first interval mark should be rendered
            FindStartingRulerMark( StartValue, out currentValue, out currentScreenOffset );

            int currentMajorMarkIndex;
            int currentMinorMarkIndex;
            FindMarkIndices( currentValue, out currentMajorMarkIndex, out currentMinorMarkIndex );

            // clear the background
            RenderBackground( dc );
            RenderRulerLine( dc );

            double rulerSize = GetRulerLenght();

            // Render all interval marks and values
            while ( ( HasHorizontalLayout() && ( currentScreenOffset <= GetRulerLenght() ) ) ||
                    ( !HasHorizontalLayout() && currentScreenOffset >= 0 ) )
            {
                RenderIntervalMark( dc, currentValue, currentScreenOffset, currentMinorMarkIndex );

                if ( HasHorizontalLayout() )
                {
                    currentScreenOffset += MinorIntervalSizePx;
                }
                else
                {
                    currentScreenOffset -= MinorIntervalSizePx;
                }

                currentMinorMarkIndex = ( currentMinorMarkIndex + 1 ) % MajorIntervalDivision;

                // check for major interval
                if ( currentMinorMarkIndex == 0 )
                {
                    currentMajorMarkIndex++;
                    currentValue = currentMajorMarkIndex * MajorIntervalUnit;
                }
            }
        }
        #endregion

        #region Protected render functions
        private void RenderRulerLine( DrawingContext dc )
        {
            if ( !DrawRulerLine )
            {
                return;
            }

            if ( HasHorizontalLayout() )
            {
                double size = GetRulerLenght();
                dc.DrawLine( _rulerPen, _basePoint, new Point( _basePoint.X + size, _basePoint.Y ) );
            }
            else
            {
                dc.DrawLine( _rulerPen, _basePoint, new Point( _basePoint.X, 0 ) );
            }
        }

        private void RenderIntervalMark( DrawingContext dc, double value, double screenPosition, int markIndex )
        {
            MarkType type = GetIntervalMarkType( markIndex );

            Pen pen = GetIntervalPen( markIndex );
            Size size = GetIntervalSize( markIndex );

            // find start and end points for the helper line
            double startX = _basePoint.X;
            double startY = _basePoint.Y;

            if ( Layout == LayoutTypes.Bottom || Layout == LayoutTypes.Top )
            {
                startX += screenPosition;
            }
            else
            {
                startY = screenPosition;
            }

            double endX = startX;
            double endY = startY;

            if ( Layout == LayoutTypes.Bottom )
            {
                endY -= size.Height;
            }
            else if ( Layout == LayoutTypes.Right )
            {
                endX -= size.Width;
            }
            else
            {
                endX += size.Width;
                endY += size.Height;
            }

            Point endPoint = new Point( endX, endY );
            dc.DrawLine( pen, new Point( startX, startY ), endPoint );

            if ( type == MarkType.Major )
            {
                // add a value stamp label for a major line
                RenderValue( dc, value, endPoint );
            }
        }

        private void RenderBackground( DrawingContext dc )
        {
            dc.DrawRectangle( Background, null, new Rect( 0, 0, base.ActualWidth, base.ActualHeight ) );

            if ( DrawValidRange )
            {
                bool maxVisible = IsValueVisibleOnScreen( MaxValue );
                bool minVisible = IsValueVisibleOnScreen( MinValue );

                if ( maxVisible && minVisible )
                {
                    double maxXPosition = ValueToScreenOffset( MaxValue );
                    double minXPosition = ValueToScreenOffset( MinValue );

                    dc.DrawRectangle( ValidRangeBackground, null, new Rect( minXPosition, 0, maxXPosition - minXPosition, base.ActualHeight ) );
                }
                else if ( maxVisible )
                {
                    double maxXPosition = ValueToScreenOffset( MaxValue );

                    dc.DrawRectangle( ValidRangeBackground, null, new Rect( 0, 0, maxXPosition, base.ActualHeight ) );
                }
                else if ( minVisible )
                {
                    double minXPosition = ValueToScreenOffset( MinValue );

                    dc.DrawRectangle( ValidRangeBackground, null, new Rect( minXPosition, 0, base.ActualWidth, base.ActualHeight ) );
                }
            }
        }

        private void RenderValue( DrawingContext dc, double value, Point textPosition )
        {
            if ( !DrawValues )
                return;

            string text = value.ToString();
            FormattedText formattedText = new FormattedText( text, _fontCultureInfo, FlowDirection.LeftToRight, _fontFace, FontSize, Brushes.Black );
            Size textSize = new Size( formattedText.Width, formattedText.Height );

            if ( HasHorizontalLayout() )
            {
                textPosition.X -= textSize.Width / 2;
                if ( Layout == LayoutTypes.Bottom )
                {
                    textPosition.Y -= textSize.Height;
                }

                // Set a maximum width and height. If the text overflows these values, an ellipsis "..." appears.
                formattedText.MaxTextWidth = MajorIntervalSizePx;
                formattedText.MaxTextHeight = textSize.Height;
            }
            else
            {
                textPosition.Y -= textSize.Height / 2;
                if ( Layout == LayoutTypes.Right )
                {
                    textPosition.X -= textSize.Width;
                }
                textPosition.X -= 2;

                formattedText.MaxTextWidth = base.ActualWidth;
                formattedText.MaxTextHeight = textSize.Height;
            }

            formattedText.SetForegroundBrush( Foreground );

            // Draw the formatted text string to the DrawingContext of the control.
            dc.DrawText( formattedText, textPosition );
        }
        #endregion

        #region Protected helper functions
        protected void SetRulerState()
        {
            MinorIntervalSizePx = MajorIntervalSizePx / (double)MajorIntervalDivision;
            MinorIntervalUnit = MajorIntervalUnit / (double)MajorIntervalDivision;

            _dispalyMiddleMark = ( MajorIntervalDivision % 2 == 0 );
            _pixelToValueRatio = MajorIntervalSizePx / MajorIntervalUnit;
        }

        private double GetRulerLenght()
        {
            if ( HasHorizontalLayout() )
            {
                return base.ActualWidth;
            }
            else
            {
                return base.ActualHeight;
            }
        }

        private void UpdateCurrentEndValue()
        {
            CurrentEndValue = StartValue + ( GetRulerLenght() / MajorIntervalSizePx ) * MajorIntervalUnit;
        }

        private void RecomputeRulerStartingPosion()
        {
            switch ( Layout )
            {
                case LayoutTypes.Top:
                    _basePoint = new Point( 0, 0 );
                    break;
                case LayoutTypes.Bottom:
                    _basePoint = new Point( 0, base.ActualHeight );
                    break;
                case LayoutTypes.Right:
                    _basePoint = new Point( base.ActualWidth, base.ActualHeight );
                    break;
                case LayoutTypes.Left:
                    _basePoint = new Point( 0, base.ActualHeight );
                    break;
                default:
                    _basePoint = new Point( 0, 0 );
                    break;
            }
        }

        private enum MarkType
        {
            Major,
            Middle,
            Minor
        }

        private MarkType GetIntervalMarkType( int markIndex )
        {
            // find mark line size
            bool isMajorMark = ( markIndex == 0 );
            if ( isMajorMark )
            {
                return MarkType.Major;
            }
            else
            {
                if ( _dispalyMiddleMark )
                {
                    bool isMidMark = ( markIndex == ( MajorIntervalDivision / 2 ) );
                    if ( isMidMark )
                    {
                        return MarkType.Middle;
                    }
                }
            }
            return MarkType.Minor;
        }

        private Pen GetIntervalPen( int markIndex )
        {
            Pen pen = null;
            MarkType type = GetIntervalMarkType( markIndex );
            switch ( type )
            {
                case MarkType.Major: pen = _rulerMajorIntervalPen; break;
                case MarkType.Middle: pen = _rulerMiddleIntervalPen; break;
                case MarkType.Minor: pen = _rulerMinorIntervalPen; break;
            }

            return pen;
        }

        private Size GetIntervalSize( int markIndex )
        {
            double size = 0;
            if ( DrawStretchedIntervalMarks )
            {
                size = HasHorizontalLayout() ? base.ActualHeight : base.ActualWidth;
            }
            else
            {
                MarkType type = GetIntervalMarkType( markIndex );
                switch ( type )
                {
                    case MarkType.Major: size = _majorMarkSizePx; break;
                    case MarkType.Middle: size = _middleMarkSizePx; break;
                    case MarkType.Minor: size = _minorMarkSizePx; break;
                }
            }
            return HasHorizontalLayout() ? new Size( 0, size ) : new Size( size, 0 );
        }

        private void FindStartingRulerMark( double value, out double currentValue, out double screenOffset )
        {
            currentValue = value;
            screenOffset = 0.0f;

            if ( currentValue % MinorIntervalUnit != 0 )
            {
                // currentValue is placed somewhere between minor intervals. Snap currentValue and screenOffset to proper value.
                double fractional = Math.Abs( ( currentValue % MinorIntervalUnit ) / MinorIntervalUnit );
                if ( currentValue > 0 )
                    fractional = 1.0 - fractional;

                screenOffset = fractional * MinorIntervalSizePx;
                currentValue = Math.Ceiling( currentValue / MinorIntervalUnit ) * MinorIntervalUnit;
                currentValue = Math.Round( currentValue, 5 );
            }

            if ( Layout == LayoutTypes.Right || Layout == LayoutTypes.Left )
            {
                screenOffset = _basePoint.Y - screenOffset;
            }
        }

        private void FindMarkIndices( double value, out int currentMajorMarkIndex, out int currentMinorMarkIndex )
        {
            double minorIntervalValue = Math.Round( ( ( Math.Abs( value ) % MajorIntervalUnit ) / MinorIntervalUnit ), 5 );
            currentMinorMarkIndex = (int)Math.Ceiling( minorIntervalValue );

            if ( value < 0 && currentMinorMarkIndex != 0 )
            {
                currentMinorMarkIndex = MajorIntervalDivision - currentMinorMarkIndex;
            }

            currentMajorMarkIndex = (int)( value / MajorIntervalUnit );
            if ( value < 0 && currentMinorMarkIndex != 0 )
            {
                currentMajorMarkIndex--;
            }
        }

        private Size MeasureValueLabelSize( double value )
        {
            string text = value.ToString();

            FormattedText formattedText = new FormattedText( text, _fontCultureInfo, FlowDirection.LeftToRight, _fontFace, FontSize, Brushes.Black );
            Size size = new Size( formattedText.Width, formattedText.Height );

            if ( HasHorizontalLayout() )
            {
                size.Height += _majorMarkSizePx;
            }
            else
            {
                size.Width += _majorMarkSizePx + 2;
            }
            return size;
        }

        private bool HasHorizontalLayout()
        {
            return Layout == LayoutTypes.Bottom || Layout == LayoutTypes.Top;
        }
        #endregion

        #region Event handlers

        private void Ruler_SizeChanged( object sender, SizeChangedEventArgs e )
        {
            UpdateCurrentEndValue();
        }

        private static void UpdateEndValue( DependencyObject d, DependencyPropertyChangedEventArgs e )
        {
            Ruler ruler = d as Ruler;
            ruler.UpdateCurrentEndValue();
        }

        #endregion Event handlers
    }
}
